<script setup lang="ts" generic="TData = any">

import { VueFrameworkComponentWrapper } from '@/components/VueFrameworkComponentWrapper';
import { VueFrameworkOverrides } from '@/components/VueFrameworkOverrides';
import type { Props } from '@/components/utils';
import { debounce, deepToRaw, getProps } from '@/components/utils';
import type { Ref } from 'vue';
import { getCurrentInstance, markRaw, onMounted, onUnmounted, ref, toRaw, toRefs, useTemplateRef, watch } from 'vue';

import type { AgEventType, GridApi, GridOptions, IRowNode } from 'ag-grid-community';
import {
    ALWAYS_SYNC_GLOBAL_EVENTS,
    _registerModule,
    RowApiModule,
    _PUBLIC_EVENT_HANDLERS_MAP,
    _GET_ALL_GRID_OPTIONS,
    _combineAttributesAndGridOptions,
    _processOnChange,
    _warn,
    createGrid,
} from 'ag-grid-community';

const props = withDefaults(defineProps<Props<TData>>(), getProps());

const rootRef = useTemplateRef<HTMLDivElement>('root');

const api: Ref<GridApi | undefined> = ref(undefined);
const gridCreated: Ref<boolean> = ref(false);
const isDestroyed: Ref<boolean> = ref(false);
const gridReadyFired: Ref<boolean> = ref(false);
const batchChanges: Ref<{ [key: string]: any }> = ref({});
const batchTimeout: Ref<number | null> = ref(null);

// setup up watches
const propsAsRefs = toRefs<any>(props);
_GET_ALL_GRID_OPTIONS()
    .filter((propertyName: string) => propertyName != 'gridOptions') // dealt with in AgGridVue itself
    .forEach((propertyName: string) => {
        watch(
            () => propsAsRefs[propertyName],
            (oldValue: any, newValue: any) => {
                if((propertyName === "rowData" && !emittingRowData.value) ||
                    propertyName !== "rowData") {
                    processChanges(propertyName, oldValue, newValue);
                }
                emittingRowData.value = false;
            },
            { deep: true }
        );
    });

// v-model code start
const ROW_DATA_EVENTS: Set<string> = new Set(['rowDataUpdated', 'cellValueChanged', 'rowValueChanged']);
const rowDataModel = defineModel<TData[]>();
const rowDataUpdating: Ref<boolean> = ref(false);
const emittingRowData: Ref<boolean> = ref(false);
const emits = defineEmits<{
    'update:modelValue': [event: TData[]];
}>();
watch(
    rowDataModel,
    (newValue: any, oldValue: any) => {
        if (gridCreated.value) {
            if(!emittingRowData.value) {
                rowDataUpdating.value = true;
                processChanges('rowData', deepToRaw(newValue), deepToRaw(oldValue));
            }
            emittingRowData.value = false
        }
    },
    { deep: true }
);

const emitRowModel = debounce(() => {
    emittingRowData.value = true;
    emits('update:modelValue', getRowData());
}, 10);

const thisInstance = getCurrentInstance();

const updateModelIfUsed = (eventType: string) => {
    if (gridReadyFired.value && ROW_DATA_EVENTS.has(eventType)) {
        if (thisInstance?.vnode?.props?.["onUpdate:modelValue"]) {
            emitRowModel();
        }
    }
};
// v-model code end

const checkForBindingConflicts = () => {
    if ((props.rowData || props.gridOptions.rowData) && rowDataModel.value) {
        _warn(232);
    }
};

const getRowDataBasedOnBindings = () => {
    return rowDataModel.value || props.rowData || props.gridOptions.rowData;
};

const getRowData = (): TData[] => {
    const rowData: any[] = [];
    api?.value!.forEachLeafNode((rowNode: IRowNode) => {
        rowData.push(rowNode.data);
    });
    return rowData;
};

const globalEventListenerFactory = (restrictToSyncOnly?: boolean) => {
    return (eventType: AgEventType) => {
        if (isDestroyed.value) {
            return;
        }

        if (eventType === 'gridReady') {
            gridReadyFired.value = true;
        }

        const alwaysSync = ALWAYS_SYNC_GLOBAL_EVENTS.has(eventType);
        if ((alwaysSync && !restrictToSyncOnly) || (!alwaysSync && restrictToSyncOnly)) {
            return;
        }

        if (ROW_DATA_EVENTS.has(eventType)) {
            if (!rowDataUpdating.value) {
                updateModelIfUsed(eventType);
            }
            rowDataUpdating.value = false;
        }
    };
};

const processChanges = (propertyName: string, currentValue: any, previousValue: any) => {
    if (gridCreated.value) {
        let value = currentValue.value || currentValue;
        if (propertyName === 'rowData' && value != undefined) {
            // Prevent the grids internal edits from being reactive
            value = deepToRaw<TData[]>(value);
        }

        batchChanges.value[propertyName] = value;
        if (batchTimeout.value == null) {
            batchTimeout.value = window.setTimeout(() => {
                // Clear the timeout before processing the changes in case processChanges triggers another change.
                batchTimeout.value = null;
                _processOnChange(batchChanges.value, api.value!);
                batchChanges.value = {};
            }, 0);
        }
    }
};

const getProvides = () => {
    return Object.create((getCurrentInstance() as any).provides);
};

onMounted(() => {
    // Row API module is required for getRowData to work
    _registerModule(RowApiModule,undefined, true);
    const frameworkComponentWrapper = new VueFrameworkComponentWrapper(getCurrentInstance(), getProvides());

    const gridParams = {
        globalListener: globalEventListenerFactory(),
        globalSyncListener: globalEventListenerFactory(true),
        frameworkOverrides: new VueFrameworkOverrides(getCurrentInstance()),
        providedBeanInstances: {
            frameworkCompWrapper: frameworkComponentWrapper,
        },
        modules: props.modules,
    };

    const gridOptions = markRaw(
        _combineAttributesAndGridOptions(deepToRaw<GridOptions<TData>>(props.gridOptions), props, [
            ..._GET_ALL_GRID_OPTIONS(),
            // we could have replaced it with GRID_OPTIONS_VALIDATORS().allProperties,
            // but that prevents tree shaking of validation code in Vue
            ...Object.values(_PUBLIC_EVENT_HANDLERS_MAP),
        ])
    );

    const rowData = getRowDataBasedOnBindings();
    if (rowData !== undefined) {
        gridOptions.rowData = deepToRaw(rowData as TData[]);
    }

    api.value = createGrid(rootRef.value!, gridOptions, gridParams);
    gridCreated.value = true;
});

onUnmounted(() => {
  if (gridCreated.value) {
    api?.value?.destroy();
    isDestroyed.value = true;
  }
});

defineExpose({
    api,
});
</script>

<template>
    <div ref="root"></div>
</template>

<style scoped></style>
